﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/GameAreaViewModel.cs
//  *
//  ***************************************************************************

#endregion

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.Threading;

using Haplous.Core;

using JetBrains.Annotations;

namespace Haplous.Presentation.ViewModel
{
    /// <summary>
    ///     The view model of area in which the game will be played.
    /// </summary>
    [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")]
    [DebuggerDisplay("GameAreaViewModel: Size={_actualSize.Width},{_actualSize.Height}")]
    [UsedImplicitly]
    internal class GameAreaViewModel : DisposableNotifyObject
    {
        /// <summary>
        ///     The actual size (in pixels) of the area in which the game is being displayed.
        /// </summary>
        [NotNull]
        private readonly Size<double> _actualSize = new Size<double>(Double.NaN, Double.NaN);

        /// <summary>
        ///     A lock controlling concurrent access to the <see cref="_visualElements" /> field.
        /// </summary>
        [NotNull]
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

        /// <summary>
        ///     The collection of all active visual elements to display.
        /// </summary>
        /// <remarks>Concurrent access is controlled by the <see cref="_lock" /> field.</remarks>
        [NotNull]
        private readonly ObservableCollection<object> _visualElements = new ObservableCollection<object>();

        /// <summary>
        ///     A read-only wrapper around the <see cref="_visualElements" /> field.
        /// </summary>
        [NotNull]
        private readonly ReadOnlyObservableCollection<object> _visualElementsReadOnly;

        /// <summary>
        ///     The background image to display.
        /// </summary>
        private Image _backgroundImage;

        /// <summary>
        ///     The game to play.
        /// </summary>
        [CanBeNull]
        private Game _game;

        /// <summary>
        ///     Initializes a new instance of the <see cref="GameAreaViewModel" /> class.
        /// </summary>
        public GameAreaViewModel()
        {
            _visualElementsReadOnly = new ReadOnlyObservableCollection<object>(_visualElements);
            _actualSize.PropertyChanged += SizeChanged;
        }

        /// <summary>
        ///     Gets the factor by which all X dimensions and coordinates must be scaled to map from the nominal to the actual size
        ///     of the game.
        /// </summary>
        /// <value>The horizontal scaling factor.</value>
        public double ScaleX
        {
            get
            {
                if(_game == null)
                    return 1d;
                return (_actualSize.Width / _game.Size.Width);
            }
        }

        /// <summary>
        ///     Gets the factor by which all Y dimensions and coordinates must be scaled to map from the nominal to the actual size
        ///     of the game.
        /// </summary>
        /// <value>The vertical scaling factor.</value>
        public double ScaleY
        {
            get
            {
                if(_game == null)
                    return 1d;
                return (_actualSize.Height / _game.Size.Height);
            }
        }

        /// <summary>
        ///     Gets the nominal size of the game.
        /// </summary>
        /// <value>The nominal size of the game.</value>
        public SizeInt32 GameSize
        {
            get
            {
                if(_game == null)
                    return null;
                return _game.Size;
            }
        }

        /// <summary>
        ///     A read-only observable collection of all active visual elements to display.
        /// </summary>
        [NotNull]
        public ReadOnlyObservableCollection<object> VisualElements
        {
            get { return _visualElementsReadOnly; }
        }

        /// <summary>
        ///     The actual size (in pixels) of the area in which the game is being displayed.
        /// </summary>
        [NotNull]
        public Size<double> ActualSize
        {
            get { return _actualSize; }
        }

        /// <summary>
        ///     The game to play.
        /// </summary>
        /// <value>The game.</value>
        [CanBeNull]
        public Game Game
        {
            get { return _game; }
            set
            {
                var oldGame = _game;
                if(!OnPropertyChanged("Game", ref _game, value))
                    return;
                if(oldGame != null)
                {
                    oldGame.Size.PropertyChanged -= SizeChanged;
                    Dispatcher.Invoke(TerminateVisualElements);
                    oldGame.Stop();
                }

                if(_game != null)
                {
                    _game.Start();
                    Dispatcher.Invoke(InitializeVisualElements);
                    _game.Size.PropertyChanged += SizeChanged;
                }
            }
        }

        /// <summary>
        ///     The background image to display.
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [CanBeNull]
        [UsedImplicitly]
        public Image BackgroundImage
        {
            get { return _backgroundImage; }
            private set { OnPropertyChanged("BackgroundImage", ref _backgroundImage, value); }
        }

        /// <summary>
        ///     Called when the actual or nominal size of the view area changes.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="PropertyChangedEventArgs" /> instance containing the event data.</param>
        private void SizeChanged([CanBeNull] object sender, [NotNull] PropertyChangedEventArgs e)
        {
            switch(e.PropertyName)
            {
                case "Width":
                    OnPropertyChanged("ScaleX");
                    break;
                case "Height":
                    OnPropertyChanged("ScaleY");
                    break;
            }
        }

        /// <summary>
        ///     Initializes all the WPF visual elements that will be used to represent the game.
        /// </summary>
        private void InitializeVisualElements()
        {
            var uiElements = new List<object>();
            _lock.EnterWriteLock();
            try
            {
                var z = 0;
                foreach(var layer  in _game.Layers)
                {
                    foreach(var element in layer.Elements)
                    {
                        object ui;
                        var background = element as Background;
                        if(background != null)
                            InitializeBackground(background);
                        else
                        {
                            var floating = element as IGraphicBlock;
                            if(floating != null)
                                ui = InitializeGraphicBlock(floating, z);
                            else
                            {
                                var text = element as ITextBlock;
                                if(text != null)
                                    ui = InitializeTextBlock(text, z);
                                else
                                    throw new HaplousException(string.Format(CultureInfo.InvariantCulture, "Unknown element type: \"{0}.{1}\".", element.GetType().Namespace, element.GetType().Name));
                            }

                            z++;

                            if(ui != null)
                                uiElements.Add(ui);
                        }
                    }
                }
                foreach(var ui in uiElements)
                    _visualElements.Add(ui);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        ///     Initializes a text block to display in the UI.
        /// </summary>
        /// <param name="text">The text block for which to provide a UI element.</param>
        /// <param name="zOrder">The z-order at which the element is to be displayed.</param>
        /// <returns>The UI element that will be used to display the text block given.</returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        [NotNull]
        private object InitializeTextBlock([NotNull] ITextBlock text, int zOrder)
        {
            return new TextBlockWrapper(this, text, zOrder);
        }

        /// <summary>
        ///     Initializes a background to display in the UI.
        /// </summary>
        /// <param name="background">The background element for which to provide a UI element.</param>
        /// <returns>The UI element that will be used to display the background given.</returns>
        [NotNull]
        private void InitializeBackground([NotNull] Background background)
        {
            background.PictureSource.PropertyChanged += BackgroundImageChanged;
            BackgroundImage = background.PictureSource.ActiveImage;
        }

        private void BackgroundImageChanged(object sender, PropertyChangedEventArgs e)
        {
            var background = sender as Background;
            if(background == null)
                return;

            if(e.PropertyName == "ActiveImage")
                BackgroundImage = background.PictureSource.ActiveImage;
        }

        /// <summary>
        ///     Initializes a graphic block to display in the UI.
        /// </summary>
        /// <param name="solid">The graphic block for which to provide a UI element.</param>
        /// <param name="zOrder">The z-order at which the element is to be displayed.</param>
        /// <returns>The UI element that will be used to display the graphic block given.</returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        [NotNull]
        private object InitializeGraphicBlock([NotNull] IGraphicBlock solid, int zOrder)
        {
            return new GraphicBlockWrapper(this, solid, zOrder);
        }

        /// <summary>
        ///     Terminates all the WPF visual elements that will be used to represent the game.
        /// </summary>
        private void TerminateVisualElements()
        {
            _lock.EnterWriteLock();
            try
            {
                _visualElements.Clear();
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        ///     Called when the control hosting the game is unloaded.
        /// </summary>
        public void Unloaded()
        {
            if(_game != null)
                _game.Stop();
        }
    }
}